OneToManyMetadataResolver.java
package org.codefilarete.stalactite.engine.configurer.dslresolver;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.Mutator;
import org.codefilarete.reflection.PropertyMutator;
import org.codefilarete.reflection.ReversibleMutator;
import org.codefilarete.stalactite.dsl.MappingConfigurationException;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy.ReferencedColumnNames;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.ForeignKeyNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.JoinColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.property.CascadeOptions;
import org.codefilarete.stalactite.engine.configurer.NamingConfiguration;
import org.codefilarete.stalactite.engine.configurer.dslresolver.InheritanceConfigurationResolver.ResolvedConfiguration;
import org.codefilarete.stalactite.engine.configurer.dslresolver.MetadataSolvingCache.EntitySource;
import org.codefilarete.stalactite.engine.configurer.model.DirectRelationJoin;
import org.codefilarete.stalactite.engine.configurer.model.Entity;
import org.codefilarete.stalactite.engine.configurer.model.IntermediaryRelationJoin;
import org.codefilarete.stalactite.engine.configurer.model.RelationJoin;
import org.codefilarete.stalactite.engine.configurer.model.ResolvedOneToManyRelation;
import org.codefilarete.stalactite.engine.configurer.onetomany.OneToManyRelation;
import org.codefilarete.stalactite.engine.runtime.AssociationTable;
import org.codefilarete.stalactite.engine.runtime.IndexedAssociationTable;
import org.codefilarete.stalactite.sql.ConnectionConfiguration;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.ForeignKey;
import org.codefilarete.stalactite.sql.ddl.structure.Key;
import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.bean.Objects;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;
import static org.codefilarete.tool.Nullable.nullable;
import static org.codefilarete.tool.collection.Iterables.first;
public class OneToManyMetadataResolver {
private final Dialect dialect;
private final ConnectionConfiguration connectionConfiguration;
public OneToManyMetadataResolver(Dialect dialect, ConnectionConfiguration connectionConfiguration) {
this.dialect = dialect;
this.connectionConfiguration = connectionConfiguration;
}
<C, I> Set<EntitySource<?, ?>> resolve(EntitySource<C, I> source) {
KeepOrderSet<EntitySource<?, ?>> targetEntities = new KeepOrderSet<>();
// configuring one-to-manys owned by this entity
source.getResolvedConfigurations().forEach(resolvedConfiguration -> {
targetEntities.addAll(resolve(source.getEntity(), resolvedConfiguration.getMappingConfiguration()));
});
return targetEntities;
}
private <C, I> Set<EntitySource<?, ?>> resolve(Entity<C, I, ?> entity, EntityMappingConfiguration<C, I> mappingConfiguration) {
KeepOrderSet<EntitySource<?, ?>> targetEntities = new KeepOrderSet<>();
mappingConfiguration.getOneToManys().forEach(oneToMany -> {
EntitySource<Object, Object> resolve = this.resolve(entity, oneToMany);
targetEntities.add(resolve);
});
// treating relations embedded in insets
mappingConfiguration.getPropertiesMapping().getInsets().forEach(inset -> {
inset.getConfigurationProvider().getConfiguration().getOneToManys().forEach(oneToMany -> {
EntitySource<Object, Object> resolve = this.resolve(entity, oneToMany.embedInto(inset.getAccessor(), inset.getEmbeddedClass()));
targetEntities.add(resolve);
});
});
return targetEntities;
}
<SRC, TRGT, S extends Collection<TRGT>, SRCID, TRGTID, SRCTABLE extends Table<SRCTABLE>, TRGTTABLE extends Table<TRGTTABLE>>
EntitySource<TRGT, TRGTID> resolve(Entity<SRC, SRCID, SRCTABLE> source, OneToManyRelation<SRC, TRGT, TRGTID, S> oneToMany) {
EntitySource<TRGT, TRGTID> targetEntitySource = buildTargetEntity(oneToMany);
NamingConfiguration namingConfiguration = first(targetEntitySource.getResolvedConfigurations()).getNamingConfiguration();
PropertyMutator<TRGT, SRC> reverseAccessPoint = oneToMany.giveReverseSetter();
RelationJoin tablesJoin = null;
BeanRelationFixer<SRC, TRGT> relationFixer;
Entity<TRGT, TRGTID, TRGTTABLE> targetEntity = targetEntitySource.getEntity();
AccessorDefinition collectionAccessorDefinition = AccessorDefinition.giveDefinition(oneToMany.getCollectionAccessor());
Supplier<S> collectionFactory = oneToMany.getCollectionFactory();
if (collectionFactory == null) {
collectionFactory = Reflections.giveCollectionFactory((Class<S>) collectionAccessorDefinition.getMemberType());
}
AccessorDefinition accessorDefinitionForTableNaming = new AccessorDefinition(
collectionAccessorDefinition.getDeclaringClass(),
collectionAccessorDefinition.getName(),
// we prefer the target persister type to method reference member type because the latter only gets the collection type which is not
// valuable information for table / column naming
oneToMany.getTargetMappingConfiguration().getEntityType());
Column<TRGTTABLE, Integer> indexingColumn = null;
if (oneToMany.isOwnedByReverseSide()) {
// target owns the relation
// we don't create foreign key for table-per-class because source columns should reference different tables (the on per entity) which databases do not allow
boolean canCreateForeignKey = !source.isTablePerClass();
if (canCreateForeignKey) {
if (!targetEntity.isTablePerClass()) {
OneToManyOwnedByTargetHelper<SRC, TRGT, SRCID, TRGTID, SRCTABLE, TRGTTABLE> helper = new OneToManyOwnedByTargetHelper<>();
tablesJoin = helper.determineJoin(oneToMany, source.getTable().getPrimaryKey(), targetEntity.getTable(), namingConfiguration.getJoinColumnNamingStrategy(), namingConfiguration.getForeignKeyNamingStrategy());
}
} // else: creating foreign key is not possible, nothing special to do
Mutator<TRGT, SRC> NOOP_REVERSE_SETTER = (o, i) -> {};
relationFixer = BeanRelationFixer.of(oneToMany.getCollectionAccessor(), collectionFactory, Objects.preventNull(oneToMany.getReverseLink(), NOOP_REVERSE_SETTER));
if (oneToMany.isOrdered()) {
indexingColumn = oneToMany.getIndexingColumn();
if (indexingColumn == null) {
String indexingColumnName = nullable(oneToMany.getIndexingColumnName()).getOr(() -> namingConfiguration.getIndexColumnNamingStrategy().giveName(accessorDefinitionForTableNaming));
Class indexColumnType = oneToMany.getRelationMode() == CascadeOptions.RelationMode.ALL_ORPHAN_REMOVAL
? int.class
: Integer.class; // column must be nullable since row won't be deleted through orphan removal but only "detached" from parent row
indexingColumn = targetEntity.getTable().addColumn(indexingColumnName, indexColumnType);
}
}
} else {
// an association table is necessary to link source and target entities
// we don't create foreign key for table-per-class because source columns should reference different tables (the on per entity) which databases do not allow
boolean canCreateForeignKey = !source.isTablePerClass();
if (canCreateForeignKey) {
if (!targetEntity.isTablePerClass()) {
OneToManyWithAssociationTableHelper<SRC, TRGT, SRCID, TRGTID, SRCTABLE, TRGTTABLE, ?> helper = new OneToManyWithAssociationTableHelper<>(accessorDefinitionForTableNaming);
tablesJoin = helper.determineJoin(
oneToMany,
source.getTable().getPrimaryKey(),
targetEntity.getTable(),
namingConfiguration.getAssociationTableNamingStrategy(),
namingConfiguration.getForeignKeyNamingStrategy(),
namingConfiguration.getIndexColumnNamingStrategy());
}
} // else: creating foreign key is not possible, nothing special to do
Mutator<TRGT, SRC> NOOP_REVERSE_SETTER = (o, i) -> {};
relationFixer = BeanRelationFixer.of(oneToMany.getCollectionAccessor(), collectionFactory, Objects.preventNull(oneToMany.getReverseLink(), NOOP_REVERSE_SETTER));
if (oneToMany.isOrdered()) {
indexingColumn = oneToMany.getIndexingColumn();
if (indexingColumn == null) {
String indexingColumnName = nullable(oneToMany.getIndexingColumnName()).getOr(() -> namingConfiguration.getIndexColumnNamingStrategy().giveName(accessorDefinitionForTableNaming));
Class indexColumnType = oneToMany.getRelationMode() == CascadeOptions.RelationMode.ALL_ORPHAN_REMOVAL
? int.class
: Integer.class; // column must be nullable since row won't be deleted through orphan removal but only "detached" from parent row
indexingColumn = targetEntity.getTable().addColumn(indexingColumnName, indexColumnType);
}
}
}
ResolvedOneToManyRelation<SRC, TRGT, S, SRCID, TRGTID, SRCTABLE, TRGTTABLE> entitiesLink = new ResolvedOneToManyRelation<>(
targetEntity,
oneToMany.getCollectionAccessor(),
oneToMany.getRelationMode(),
oneToMany.isOwnedByReverseSide(),
reverseAccessPoint,
oneToMany.isFetchSeparately(),
tablesJoin,
relationFixer,
collectionFactory,
indexingColumn
);
source.addRelation(entitiesLink);
return targetEntitySource;
}
private <SRC, TRGT, S extends Collection<TRGT>, TRGTID> EntitySource<TRGT, TRGTID>
buildTargetEntity(OneToManyRelation<SRC, TRGT, TRGTID, S> oneToMany) {
InheritanceConfigurationResolver<TRGT, TRGTID> inheritanceConfigurationResolver = new InheritanceConfigurationResolver<>();
KeepOrderSet<ResolvedConfiguration<?, TRGTID>> ancestorsConfigurations = inheritanceConfigurationResolver.resolveConfigurations(oneToMany.getTargetMappingConfiguration());
InheritanceMetadataResolver<TRGT, TRGTID, ?> keyMappingApplier = new InheritanceMetadataResolver<>(dialect, connectionConfiguration);
return keyMappingApplier.resolve(ancestorsConfigurations);
}
private static class OneToManyOwnedByTargetHelper<SRC, TRGT, SRCID, TRGTID, LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>> {
protected RelationJoin determineJoin(OneToManyRelation<SRC, TRGT, TRGTID, ?> relation,
PrimaryKey<LEFTTABLE, SRCID> leftPrimaryKey,
RIGHTTABLE targetTable,
JoinColumnNamingStrategy joinColumnNamingStrategy,
ForeignKeyNamingStrategy foreignKeyNamingStrategy) {
ForeignKey<RIGHTTABLE, LEFTTABLE, SRCID> foreignKey = determineForeignKeyColumns(relation, leftPrimaryKey, targetTable, joinColumnNamingStrategy, foreignKeyNamingStrategy);
return new DirectRelationJoin<>(foreignKey.getReferencedKey(), foreignKey);
}
private ForeignKey<RIGHTTABLE, LEFTTABLE, SRCID> determineForeignKeyColumns(OneToManyRelation<SRC, TRGT, TRGTID, ?> relation,
PrimaryKey<LEFTTABLE, SRCID> leftPrimaryKey,
RIGHTTABLE targetTable,
JoinColumnNamingStrategy joinColumnNamingStrategy,
ForeignKeyNamingStrategy foreignKeyNamingStrategy) {
Key.KeyBuilder<RIGHTTABLE, SRCID> foreignKeyBuilder = Key.from(targetTable);
if (!relation.getForeignKeyNameMapping().isEmpty()) {
Map<Accessor<SRCID, ?>, Column<RIGHTTABLE, ?>> foreignKeyColumnMapping = new HashMap<>();
relation.getForeignKeyNameMapping().forEach((valueAccessPoint, colName) -> {
AccessorDefinition localAccessorDefinition = AccessorDefinition.giveDefinition(valueAccessPoint);
Accessor<SRCID, Object> accessor = valueAccessPoint instanceof Accessor
? (Accessor) valueAccessPoint
: (valueAccessPoint instanceof ReversibleMutator ? ((ReversibleMutator) valueAccessPoint).toAccessor() : null);
if (accessor == null) {
throw new UnsupportedOperationException("Can't get accessor from " + valueAccessPoint);
}
Column<RIGHTTABLE, ?> column = targetTable.addColumn(colName, localAccessorDefinition.getMemberType());
foreignKeyBuilder.addColumn(column);
foreignKeyColumnMapping.put(accessor, column);
});
} else if (!relation.getForeignKeyColumnMapping().isEmpty()) {
Map<Accessor<SRCID, ?>, Column<RIGHTTABLE, ?>> foreignKeyColumnMapping = new HashMap<>();
relation.getForeignKeyColumnMapping().forEach((valueAccessPoint, column) -> {
Accessor<SRCID, Object> accessor = valueAccessPoint instanceof Accessor
? (Accessor) valueAccessPoint
: (valueAccessPoint instanceof ReversibleMutator ? ((ReversibleMutator) valueAccessPoint).toAccessor() : null);
if (accessor == null) {
throw new UnsupportedOperationException("Can't get accessor from " + valueAccessPoint);
}
foreignKeyBuilder.addColumn((Column<RIGHTTABLE, ?>) column);
foreignKeyColumnMapping.put(accessor, (Column<RIGHTTABLE, ?>) column);
});
} else if (relation.getReverseColumnName() != null || relation.getReverseColumn() != null) {
Column<RIGHTTABLE, ?> reverseColumn;
if (relation.getReverseColumnName() != null) {
PrimaryKey<LEFTTABLE, SRCID> srcPrimaryKey = leftPrimaryKey;
// with a reverse column name, the primary key should be a single column key
if (srcPrimaryKey.isComposed()) {
throw new MappingConfigurationException("Giving reverse column whereas the primary key is composed :"
+ " primary key = [" + srcPrimaryKey.getColumns().stream().map(Column::getName).collect(Collectors.joining(", ")) + "] vs "
+ " reverse column = " + relation.getReverseColumnName());
}
Column<LEFTTABLE, ?> pk = first(srcPrimaryKey.getColumns());
reverseColumn = targetTable.addColumn(
relation.getReverseColumnName(),
pk.getJavaType(),
pk.getSize());
} else {
reverseColumn = (Column<RIGHTTABLE, ?>) relation.getReverseColumn();
}
foreignKeyBuilder.addColumn(reverseColumn);
} else if (relation.giveReverseSetter() != null) {
// Please note that here Setter is only used to get foreign key column names, not to set values on reverse side
Map<Column<LEFTTABLE, ?>, Column<RIGHTTABLE, ?>> foreignKeyColumnMapping = new HashMap<>();
PrimaryKey<LEFTTABLE, SRCID> primaryKey = leftPrimaryKey;
AccessorDefinition reverseGetterDefinition = AccessorDefinition.giveDefinition(relation.giveReverseSetter());
primaryKey.getColumns().forEach(pkColumn -> {
String colName = joinColumnNamingStrategy.giveName(reverseGetterDefinition, pkColumn);
Column<RIGHTTABLE, ?> fkColumn = targetTable.addColumn(colName, pkColumn.getJavaType(), pkColumn.getSize(), null);
foreignKeyBuilder.addColumn(fkColumn);
foreignKeyColumnMapping.put(pkColumn, fkColumn);
});
} // else : no reverse side mapped, this case can't happen since OneToManyWithMappedAssociationConfigurer is only
// invoked when reverse side is mapped (see OneToManyRelation.isOwnedByReverseSide())
Key<RIGHTTABLE, SRCID> foreignKey;
foreignKey = foreignKeyBuilder.build();
if (relation.isReverseAsMandatory() != null && relation.isReverseAsMandatory()) {
foreignKey.getColumns().stream().map(Column.class::cast).forEach(Column::notNull);
}
return targetTable.addForeignKey(foreignKeyNamingStrategy.giveName(foreignKey, leftPrimaryKey), foreignKey, leftPrimaryKey);
}
}
private static class OneToManyWithAssociationTableHelper<
SRC, TRGT, SRCID, TRGTID,
LEFTTABLE extends Table<LEFTTABLE>,
RIGHTTABLE extends Table<RIGHTTABLE>,
ASSOCIATIONTABLE extends AssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, SRCID, TRGTID>> {
private final AccessorDefinition accessorDefinitionForTableNaming;
public OneToManyWithAssociationTableHelper(AccessorDefinition accessorDefinitionForTableNaming) {
this.accessorDefinitionForTableNaming = accessorDefinitionForTableNaming;
}
protected RelationJoin determineJoin(OneToManyRelation<SRC, TRGT, TRGTID, ?> relation,
PrimaryKey<LEFTTABLE, SRCID> leftPrimaryKey,
RIGHTTABLE targetTable,
AssociationTableNamingStrategy associationTableNamingStrategy,
ForeignKeyNamingStrategy foreignKeyNamingStrategy,
ColumnNamingStrategy indexColumnNamingStrategy
) {
// we don't create foreign key for table-per-class because source columns should reference different tables (the one
// per entity) which databases do not allow
boolean createOneSideForeignKey = !relation.isSourceTablePerClassPolymorphic();
boolean createManySideForeignKey = !relation.isTargetTablePerClassPolymorphic();
PrimaryKey<RIGHTTABLE, TRGTID> rightPrimaryKey = targetTable.getPrimaryKey();
ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> columnNames = associationTableNamingStrategy.giveColumnNames(
accessorDefinitionForTableNaming,
leftPrimaryKey,
rightPrimaryKey);
if (relation.getSourceJoinColumnName() != null) {
columnNames.setLeftColumnName(Iterables.first(leftPrimaryKey.getColumns()), relation.getSourceJoinColumnName());
}
if (relation.getSourceJoinColumnName() != null) {
columnNames.setRightColumnName(Iterables.first(rightPrimaryKey.getColumns()), relation.getTargetJoinColumnName());
}
String associationTableName = nullable(relation.getAssociationTableName()).getOr(() -> associationTableNamingStrategy.giveName(accessorDefinitionForTableNaming,
leftPrimaryKey, rightPrimaryKey));
ASSOCIATIONTABLE intermediaryTable;
if (relation.isOrdered()) {
String indexingColumnName = nullable(relation.getIndexingColumnName()).getOr(() -> indexColumnNamingStrategy.giveName(accessorDefinitionForTableNaming));
intermediaryTable = (ASSOCIATIONTABLE) new IndexedAssociationTable<>(
leftPrimaryKey.getTable().getSchema(),
associationTableName,
leftPrimaryKey,
rightPrimaryKey,
columnNames,
foreignKeyNamingStrategy,
createOneSideForeignKey,
createManySideForeignKey,
indexingColumnName
);
} else {
intermediaryTable = (ASSOCIATIONTABLE) new AssociationTable<>(
leftPrimaryKey.getTable().getSchema(),
associationTableName,
leftPrimaryKey,
rightPrimaryKey,
columnNames,
foreignKeyNamingStrategy,
createOneSideForeignKey,
createManySideForeignKey
);
}
return new IntermediaryRelationJoin<>(intermediaryTable);
}
}
}